#!/bin/python3

# plot temperature data from my weather station.
# I invoke this as "temp_plotter".
# I wrote a first "draft" of this using Tk, but then
# switched to wx, which I like a lot better
#
# July 2022  Tom Trebisky
#
# In January 2024 after an upgrade to Fedora 39, I had to do
# a fair bit of maintenance as features stopped working.
#  this is python 3.12.1 -- python3-ephem 4.1.4
#
# 1 - fix a deprecation warning about utcnow()
# 2 - my "Monitor" subwindow must now be bigger to see temperature

import wx
import numpy as np
import datetime
import sys
import os
import math

# This is for sunrise/sunset times
# First dnf install python3-ephem
# Apparently this is also called "PyEphem"
import ephem

xsize = 800
ysize = 600
wsize = ( xsize, ysize )

# The right side needs about 250 pixels
right_size =250
split_pos = xsize - right_size

temp_file = "/u1/Projects/ESP8266/Projects/tmon/logs/temp_log99"
#temp_file = "/u1/Projects/ESP8266/Projects/tmon/logs/temp_dead"
demo_file = "./temp_demo_data"

# Start up showing 2 days by default
#default_days = 2
#default_days = 7
default_days = 1

# 6-24-2022 -- several bugs showed up when my Wifi access point was offline for a few days.
# exactly the same as if I had a dead battery for several days.
# -- we need to display "DEAD battery" in a prominent way in such a case.
# -- we need to display missing data up to end of plot.
# -- we need to start up, even if the range we want to display starts in
#    the middle of a "DEAD" zone.

# ----------------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------------

# Take an ephem time in ephem format (UTC) and
# return a time string in local time
def utc_to_local_str ( utc ) :
    #tz = 7 - 12
    #local = utc- datetime.timedelta(hours=tz)
    local = ephem.localtime(utc)
    return local.strftime ( "%H:%M:%S" )

# Here is a function to get sunrise/sunset times via the "ephem" package.
# another option would be to use the "astroplan" package,
# Astroplan is an "astropy affiliated" package for the planning of
# astronomical observing from Cornell University.

# A bonus tip from my buddy Tim is as follows:
# for date/time stuff I often use astropy.time to do the parsing and format
#  translating. provides a higher level interface than datetime.
# (I neglected this advice and was tangled up in the tower of Babel)

# In January of 2024 I began getting deprecation warnings about datetime.utcnow()
# You can read endless jabber about how python once allowed timezone aware timestamps
# and "naive" timestamps without a timezone that are expected to be in the local
# timezone, which is then obtained from the system.

def get_sunrise_ephem () :
    # It seems peculiar that it wants the lat/long as strings,
    # but it most definitely does.
    obs = ephem.Observer()
    obs.lat = '32.2226'
    obs.long = '-110.9747'
#    obs.date = datetime.datetime.utcnow()
    obs.date = datetime.datetime.now(datetime.UTC)

    sun = ephem.Sun(obs)

    # These are UTC in ephem date format
    sr_utc = obs.previous_rising(sun)
    ss_utc = obs.next_setting(sun)

    sr = utc_to_local_str(sr_utc)
    ss = utc_to_local_str(ss_utc)

    return sr, ss

# ----------------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------------

# As of 6-10-2022 my temperature data "log" file has about 4.5 years of data
# 78309751 bytes (78M), 2339523 lines, 1625 days, 4.5 years
# This averages to about 34 bytes per line.
# So ...
# 1 week is 7*24*60 lines -- 342,720 bytes of data
# Based on this, I see 500,000 bytes back from the end of the file
# and start reading there, rather than reading 78 megabytes from the very start.

class Temp_Data () :

    def __init__ ( self ) :

        if os.path.exists ( temp_file ) :
            self.file = temp_file
        elif os.path.exists ( demo_file ) :
            self.file = demo_file
        else :
            print ( "No data file, sorry ..." )
            sys.exit ()

        self.last_size = 0
        self.info = {}
        self.num_days = default_days
        self.initial_seek = None

        self.battery_is_ok = True

        # entirely bogus, but avoids errors
        # at startup to have values set.
        #self.xoff = 0
        #self.yoff = 0
        #self.ysize = 10
        #self.ysize = 10

    # check file size for new data
    def new_data ( self ) :

        new_size = os.path.getsize ( self.file )

        if new_size != self.last_size :
            self.last_size = new_size
            return True
        else :
            return False

    # This gives a hint of a good place to
    # seek into the file and start reading.
    def find_start ( self ) :
        pos = os.path.getsize ( self.file ) - 500000
        self.initial_seek = pos

        # We could do all this, carefully adjusting our
        # starting position, but the above is such a big
        # win already, that this doesn't seem worth it.
        #today = datetime.date.today()
        #start_time = today - datetime.timedelta(days=7-1)
        #start = xyz.strftime('%m-%d-%Y')

        #f = open ( self.file, "r" )
        #line = f.readline()
        #while line:
        #    loc = f.tell()
        #    line = f.readline()
        #    if line.startswith ( start ) :
        #        data.append ( line.rstrip() )
        #        break
        #    skip += 1

    # We used to simply read the entire file, which meant plowing
    # through years of old data, but now we seek to a location
    # nearer to the end, which does yield a significant speedup.
    def read_data ( self, xyz ) :

        # added this to attempt a speedup
        # with this:    10358  lines skipped
        # without it: 2336916  lines skipped
        if not self.initial_seek :
            self.find_start ()

        f = open ( self.file, "r" )

        start = xyz.strftime('%m-%d-%Y')

        f.seek ( self.initial_seek )

        data = []

        # XXX - this fails (bug) if there is no data
        # for that "target", which may happen if we had
        # a dead battery (or wifi access point offline) for
        # several days.

        # skip until we see the first line we want
        for line in f:
            if line.startswith ( start ) :
                data.append ( line.rstrip() )
                break

        # print ( skip, " lines skipped" )

        # read the rest to EOF, skipping trash lines
        for line in f:
            if line[0].isdigit() :
                data.append ( line.rstrip() )

        f.close ()
        return data

    # Read last line in file, see if it says "Battery DEAD"
    def check_battery ( self ) :
        pos = os.path.getsize ( self.file ) - 200
        f = open ( self.file, "r" )
        f.seek ( pos )
        for line in f:
            lline = line
        f.close ()

        #print ( "last line: ", lline )
        self.battery_is_ok = not lline.startswith ( 'Battery DEAD' )
        #if ll.startswith ( 'Battery DEAD' ) :
        #    self.battery_is_ok = False
        #else:
        #    self.battery_is_ok = True

    def is_dead ( self ) : 
        return not self.battery_is_ok

    # A data line looks like the following.
    #   04-07-2017 17:19:02 18 0 144 308 874
    # We need this to feed to numpy datetime
    #   x = np.datetime64('2019-08-19T20:05:02')
    # So, we just need to strip off the year and move
    # it to the front
    def mk_dt64 ( self, d, t ) :
        dw = d.split ( '-' )
        dd = dw[2] + '-' + dw[0] + '-' + dw[1]
        time_string = dd + "T" + t
        #print ( time_string )
        return np.datetime64 ( time_string )

    def conv_data ( self, data ) :
        yy = []
        xx = []
        for l in data :
            w = l.split()
            xx.append ( self.mk_dt64 ( w[0], w[1] ) )
            yy.append ( float(w[6])/10.0 )
            last_hum = float(w[4])/10.0

        y = np.array ( yy )
        #xx = np.arange ( 0.0, len(yy)*1.0, 1.0 )
        return ( xx, y, last_hum )

    def get_prior ( self ) :

        search = self.xx[-1] - np.timedelta64(1,'D')
        #print ('search: {}, {}'.format(search, self.xx[0]))

        if self.num_days == 1 :
            return self.prior_24 ( search )

        for i in range(len(self.xx)):
            if self.xx[i] >= search :
                return self.yy[i]

        # This should never happen
        # (but it did until we added prior_24()
        return self.yy[0]

    # This is used to gather 48 hours of data so we
    # can find the correct "prior" data from 24 hours ago
    # when we only have the current 24 hours displayed

    def prior_24 ( self, search ) :

        today = datetime.date.today()

        # In this particular case (getting 48 hours of data)
        # this actually is yesterday
        yesterday = today - datetime.timedelta(days=1)

        d48 = self.read_data ( yesterday )
        x48, y48, h48 = self.conv_data ( d48 )
        #print ( "First 48: ", x48[0] )

        for i in range(len(x48)):
            if x48[i] >= search :
                return y48[i]

        # should never happen
        return y48[0]

    # This gets called when new data arrives,
    # or when the display duration changes
    def gather_data ( self, days ) :

            if days :
                self.num_days = days
                #print ( "Set days = ", days )

            today = datetime.date.today()
            self.start_time = today - datetime.timedelta(days=self.num_days-1)

            d = self.read_data ( self.start_time )

            #print ( "Fetched data: ", len(d) )

            if len(d) < 1 :
                print ( "No data for specified range" )
                exit ()

            # We don't need battery data
            # to plot, just the current value
            w = d[-1].split()
            self.battery = float(w[3]) / 100.0

            # humidity data is just discarded
            #  except for displaying current value
            self.xx, self.yy, self.hh = self.conv_data ( d )

            self.xmin = np.amin ( self.xx )
            self.xmax = np.amax ( self.xx )
            self.xrange = self.xmax - self.xmin

            self.ymin = np.amin ( self.yy )
            self.ymax = np.amax ( self.yy )
            self.yrange = self.ymax - self.ymin


    def get_info ( self ) :

            prior = self.get_prior ()

            # We really only need to recalculate these
            #  when the day rolls over.
            sr, ss = get_sunrise_ephem ()
            self.info['sunrise'] = sr
            self.info['sunset'] = ss

            self.info['today'] = str(self.yy[-1])
            self.info['prior'] = str(prior)
            self.info['hum'] = str(self.hh)
            self.info['high'] = str(self.ymax)
            self.info['low'] = str(self.ymin)
            self.info['battery'] = str(self.battery)

            return self.info

    # Given an x position in pixel counts
    # return a time and temperature
    #def lookup ( self, x, w ) :
    def lookup ( self, x ) :

        nxy = len(self.xx)
        delta = np.timedelta64(70,'s')
        # We can calculate a time nicely based on
        # the x mouse location.
        #xf = x / w
        xf = x / self.xsize
        tx = self.xmin + self.xrange * xf
        # print ( "Lookup: ", x, tx )

        # Getting the temperature is more interesting
        # since there can be holes in the data.
        for i in range(nxy) :
            if self.xx[i] > tx :
                break

        # print ( "cur: ", self.xx[i], self.yy[i] )
        # print ( "pre: ", self.xx[i-1], self.yy[i-1] )

        ypre = None
        ycur = None

        if i == 0 :
            i = 1
        if i >= nxy :
            i = nxy - 1

        sum = 0.0
        div = 0.0
        if (tx - self.xx[i-1]) < delta :
            sum += self.yy[i-1]
            div += 1.0

        if (self.xx[i] - tx) < delta :
            sum += self.yy[i]
            div += 1.0

        if div > 0.5 :
            val = sum / div
        else :
            val = None


        td = np.datetime_as_string(tx, unit='D')
        tm = np.datetime_as_string(tx, unit='m' )

        # Nasty!
        # Date:  2022-06-10
        # Time:  2022-06-10T21:38
        # print ( "Date: ", td )
        # print ( "Time: ", tm )

        t = tx.astype(datetime.datetime)
        ttd = t.strftime ( '%m/%d/%Y')
        ttm = t.strftime ( '%H:%M')

        # Much better.
        # Date:  06/10/2022
        # Time:  06:26
        #print ( "Date: ", ttd )
        #print ( "Time: ", ttm )

        #ttt = str(val)
        ttt = f"{val:.1f}"
        #print ( "Temperature: ", str(val) )

        return ttd, ttm, ttt

    # return an array of tick information for the Y axis
    # We may generate marks from 70 to 120
    def getticks ( self ) :
        return self.tick_array

    # return an array of positions to mark the X axis.
    #def get_xticks ( self, width, lmargin ) :
    def get_xticks ( self ) :

        #today = datetime.date.today()
        #self.start_time = today - datetime.timedelta(days=self.num_days-1)
        #wid = width - lmargin

        #day = datetime.timedelta(days=1)
        day = np.timedelta64(1,'D')

        # This is a plain old Python datetime
        # but everything else is a numpy time.
        #xxx = self.start_time
        # Using this keeps us in the numpy world
        xxx = self.xx[0]

        rv = []
        for i in range ( self.num_days - 1 ) :
            xxx += day
            xf = (xxx - self.xmin) / self.xrange
            ix = self.xoff + int ( self.xsize * xf )
            rv.append ( ix )

        return rv

    # scale data for plotting on the screen
    #
    # XXX - it is awkward passing geometry from
    #  the panel object to the data object like
    #  this.  There ought to be a better way.
    #def getxy ( self, width, height, lmargin ) :
    def getxy ( self ) :
        rxy = []

        # getticks() will also use these
        self.ytop = math.ceil ( (self.ymax+1.0) / 10.0) * 10.
        self.ybot = math.floor ( (self.ymin-1.0) / 10.0) * 10.
        #print ( "Y bot, top: ", self.ybot, self.ytop )
        yrang = self.ytop - self.ybot

        # unused pixels at top/bottom of panel
        #top_margin = 20
        #bot_margin = 20

        # height in pixels that we will scale onto
        # high = height - (top_margin + bot_margin)
        #high = height
        # wid = width - lmargin

        x_expect = None

        #delta  <class 'datetime.timedelta'>
        #tol = datetime.timedelta(seconds=20)
        #delta = datetime.timedelta(seconds=60)

        #array  <class 'numpy.datetime64'>
        tol = np.timedelta64(20,'s')
        delta = np.timedelta64(60,'s')

        for i in range(len(self.xx)) :
            # Deal with holes (missing data)
            if x_expect and ( self.xx[i] > x_expect + tol) :
                #print ( "HOLE" )
                rxy.append ( None )

            #if x_expect :
            #    #while self.xx[i] < x_expect - tol or self.xx[i] > x_expect + tol :
            #    while self.xx[i] < x_expect - tol :
            #        rxy.append ( (0,9999) )

            xf = (self.xx[i] - self.xmin) / self.xrange
            #yf = (self.yy[i] - self.ymin) / self.yrange
            yf = (self.yy[i] - self.ybot) / yrang

            #print ( "xf ", xf )
            #print ( "w ", w )
            ix = self.xoff + np.int_ ( self.xsize * xf )
            iy = self.yoff + np.int_ ( self.ysize * (1.0-yf) )
            rxy.append ( (ix,iy) )

            #print ( 'array ', type(self.xx[i]) )
            #print ( 'delta ', type(delta) )
            x_expect = self.xx[i] + delta

        # Generate tick array here
        nticks = int ( self.ytop - self.ybot )

        self.tick_array = []
        y = self.ybot
        for i in range ( nticks ) :
            yf = (y - self.ybot) / yrang
            iy = self.yoff + np.int_ ( self.ysize * (1.0-yf) )
            label = str ( int(y) )
            self.tick_array.append ( (iy, label) )
            y += 10.0

        return rxy

    # This is how I keep this object informed
    # about things the left panel knows about
    def viewport ( self, xoff, yoff, xsize, ysize ) :
        self.xoff = xoff
        self.yoff = yoff
        self.xsize = xsize
        self.ysize = ysize

# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------

# wxPython has some severe brain damage as far as greying out text when you move
# the mouse out of the window.  They don't understand the concept that this is
# an information display, not some active control with interaction.
#
# https://stackoverflow.com/questions/17764457/make-disabled-text-render-black
#
# I also want to center text inside an area of fixed size, so I may as well
# dive in.  I am taking the code posted by Hesky Fisher in the above post.
#
#    Thanks Hesky !!

class MyStaticText ( wx.Control ):
    def __init__(self, parent, id=wx.ID_ANY, label="", 
                 pos=wx.DefaultPosition, size=wx.DefaultSize, 
                 style=0, validator=wx.DefaultValidator, 
                 name="MyStaticText"):
        wx.Control.__init__(self, parent, id, pos, size, style|wx.NO_BORDER, validator, name)

        wx.Control.SetLabel(self, label)
        self.InheritAttributes()
        self.SetInitialSize(size)

        self.bg_color = parent.GetBackgroundColour ()
        self.fg_color = wx.BLACK

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)

    def OnPaint(self, event):
        dc = wx.BufferedPaintDC(self)
        self.Draw(dc)

    def Draw(self, dc):
        width, height = self.GetClientSize()

        if not width or not height:
            return

        # TJT - We should get the background of the parent instead
        # backBrush = wx.Brush(wx.WHITE, wx.SOLID)
        backBrush = wx.Brush ( self.bg_color, wx.SOLID)
        dc.SetBackground(backBrush)
        dc.Clear()

        #dc.SetTextForeground(wx.BLACK)
        dc.SetTextForeground ( self.fg_color )
        dc.SetFont(self.GetFont())
        label = self.GetLabel()
        dc.DrawText(label, 0, 0)

    def SetFGColour(self, colour):
        self.fg_color = colour
        self.Refresh()

    def OnEraseBackground(self, event):
        pass

    def SetLabel(self, label):
        wx.Control.SetLabel(self, label)
        self.InvalidateBestSize()
        self.SetSize(self.GetBestSize())
        self.Refresh()

    def SetFont(self, font):
        wx.Control.SetFont(self, font)
        self.InvalidateBestSize()
        self.SetSize(self.GetBestSize())
        self.Refresh()

    def DoGetBestSize(self):
        label = self.GetLabel()
        font = self.GetFont()

        if not font:
            font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)

        dc = wx.ClientDC(self)
        dc.SetFont(font)

        textWidth, textHeight = dc.GetTextExtent(label)
        best = wx.Size(textWidth, textHeight)
        self.CacheBestSize(best)
        return best

    def AcceptsFocus(self):
        return False

# Does not work.
#    def SetForegroundColour(self, colour):
#        wx.Control.SetForegroundColour(self, colour)
#        self.Refresh()
#
#    def SetBackgroundColour(self, colour):
#        wx.Control.SetBackgroundColour(self, colour)
#        self.Refresh()

    def GetDefaultAttributes(self):
        return wx.StaticText.GetClassDefaultAttributes()

    def ShouldInheritColours(self):
        return True

# This is my wrapper on the above
# I had trouble getting this right.
# The trick is that the __init__ method does a special thing.
# it effectively returns "self" in a magic way.
# (and "self" is the value returned by the superclass.)
class EZtext ( MyStaticText ) :
        def __init__ ( self, parent, sizer, message ) :
            #rv = MyStaticText.__init__ ( self, parent, wx.ID_ANY, message )
            #sizer.Add ( rv, 1, wx.EXPAND )
            #print ( rv )
            #print ( type(rv) )

            # prefix some space here
            padded = "    " + message
            MyStaticText.__init__ ( self, parent, wx.ID_ANY, padded )
            sizer.Add ( self, 1, wx.EXPAND )

        def SetLabel ( self, msg ) :
            padded = "    " + msg
            super().SetLabel ( padded )

# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------

# This is the little box that pops up with information
# at the cursor position
# For some reason, in January 2024, I had to increase the size of the box
# from 100 to 120 to see the temperature.  It sort of looks like some
# invisible element is now using space at the top of the box.
class Monitor ( wx.Frame ) :
        def __init__ ( self, parent ) :
            #wx.Frame.__init__(self, None, wx.ID_ANY, "data", size=(100,100) )
            ##wx.Frame.__init__(self, None, wx.ID_ANY, "data", size=(100,100), style=wx.NO_BORDER )
            wx.Frame.__init__(self, None, wx.ID_ANY, "data", size=(100,120), style=wx.NO_BORDER )

            # This always returns 0,0 (as a wx.Point)
            pos = parent.GetPosition ()
            #print ( pos )
            #print ( type(pos) )

            # Given 0,0, this goes to screen center
            # non-zero values are from top left of screen
            #self.SetPosition ( wx.Point(100, 100) )
            self.SetPosition ( wx.Point(0, 0) )

            #self.SetBackgroundColour ( wx.RED )

            mp = wx.Panel ( self, -1 )
            ms = wx.BoxSizer ( wx.VERTICAL )

            bogus = "Hot --"

            EZtext ( mp, ms, " " )
            self.date = EZtext ( mp, ms, bogus )
            self.time = EZtext ( mp, ms, bogus )
            self.temp = EZtext ( mp, ms, bogus )

            mp.SetSizer ( ms )

        def update ( self, stuff ) :
            
            self.date.SetLabel ( stuff[0] )
            self.time.SetLabel ( stuff[1] )
            self.temp.SetLabel ( stuff[2] )

# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------

# The left panel has the graph
class Left_Panel ( wx.Panel ) :
        def __init__ ( self, parent, data ) :
            wx.Panel.__init__ ( self, parent )
            self.data = data

            self.lmargin = 40   # left only
            self.ymargin = 10   # top and bottom

            self.xtick_size = 10
            self.ytick_size = 5
            self.font_size = 12

            self.monitor = None
            self.monitor_up = False

            #self.SetBackgroundColour ( wx.RED )

            self.Bind ( wx.EVT_SIZE, self.onResize )

            self.Bind ( wx.EVT_PAINT, self.OnPaint )
            self.Bind ( wx.EVT_MOTION, self.OnMove )

            # left mouse click
            self.Bind(wx.EVT_LEFT_DOWN, self.OnLeft )

            self.curpos = None
            self.width = None
            self.height = None
            self.xy = None

        # We get 3 resize events just starting up.
        # we need this to refresh after resize
        # also to post width for Move checks
        def onResize ( self, event ) :
            #print ( "resize" )
            self.width = event.Size.width
            self.height = event.Size.height

            xsize = self.width-self.lmargin
            ysize = self.height - self.ymargin * 2

            self.data.viewport ( self.lmargin, self.ymargin, xsize, ysize )

            # print ( "width", event.Size.width )
            # print ( "height", event.Size.height )

            self.update ();

        # left mouse click
        def OnLeft ( self, event ) :
            #print ( "Click: ", x )

            x, _ = event.GetPosition ()

            if x < self.lmargin :
                return

            stuff = self.data.lookup ( x-self.lmargin )

            if not self.monitor :
                self.monitor = Monitor ( self )

            if self.monitor_up :
                self.monitor.Hide ()
                self.monitor_up = False
            else :
                self.monitor.Show ()
                self.monitor_up = True

            self.monitor.update ( stuff )

        def OnMove ( self, event ) :
            x,_ = event.GetPosition ()

            if x > self.lmargin and x < self.width :
                self.curpos = x
                self.SetCursor ( wx.Cursor(wx.CURSOR_BLANK) )
            else :
                self.curpos = None
                self.SetCursor ( wx.Cursor(wx.CURSOR_PENCIL) )

            if self.monitor_up :
                stuff = self.data.lookup ( x-self.lmargin )
                self.monitor.update ( stuff )

            # trigger a repaint
            self.Refresh ()

        # could this be moved rather than repainted?
        # we are already working at too low a level,
        # so that is not possible.
        def mkVline ( self, x ) :

            dc = wx.PaintDC ( self )
            dc.SetPen ( wx.Pen(wx.BLACK, 1) )

            dc.DrawLine ( x, 0, x, self.height-1 )

        def mkHline ( self, y ) :

            dc = wx.PaintDC ( self )
            dc.SetPen ( wx.Pen(wx.BLACK, 1) )

            dc.DrawLine ( self.lmargin, y, self.width-1, y )

        def xtick ( self, x ) :

            dc = wx.PaintDC ( self )
            dc.SetPen ( wx.Pen(wx.BLACK, 1) )

            dc.DrawLine ( x, self.height-self.xtick_size-1, x, self.height-1 )

        # The position is the upper left corner of the rectangle
        #  that holds the text
        def mtext ( self, yy, msg ) :

            dc = wx.PaintDC ( self )
            dc.SetPen ( wx.Pen(wx.BLACK, 1) )

            dc.DrawLine ( self.lmargin-self.ytick_size, yy, self.lmargin, yy )

            #font = wx.Font(18, wx.ROMAN, wx.ITALIC, wx.BOLD)
            font = wx.Font ( self.font_size, wx.ROMAN, wx.NORMAL, wx.NORMAL)
            dc.SetFont(font)
            tsize = dc.GetTextExtent ( msg )
            #print ( tsize )
            #x = self.lmargin + 2
            x = self.lmargin - (tsize[0] + 2 + self.ytick_size)
            y = int ( yy - tsize[1]/2 )
            dc.DrawText ( msg, x, y )

        def plot_em ( self ) :
            if not self.xy :
                return

            dc = wx.PaintDC ( self )
            dc.SetPen ( wx.Pen(wx.BLUE, 2) )

            lastxy = None

            for xy in self.xy :
                if xy and lastxy :
                    dc.DrawLine ( lastxy[0], lastxy[1], xy[0], xy[1] )
                lastxy = xy

        # We get lots of paint events, for reasons I don't understand,
        # and not simply related to cursor motion.
        def OnPaint ( self, event ) :
            #print ( "Paint!" )
            dc = wx.PaintDC ( self )
            dc.Clear ()

            if ( self.curpos ) :
                self.mkVline ( self.curpos )

            #w, h = self.GetSize()

            self.mkVline ( self.lmargin )

            #self.mtext ( 0, "140" )
            #self.mtext ( 100, "140" )
            #self.mtext ( 150, "140" )
            #self.mtext ( 250, "140" )
            # This is 164x19 pixels with a size 12 font
            # self.mtext ( 300, "Marvin the Alligator" )
            #self.mtext ( h-30, "140" )

            self.plot_em ()
            for (ii,ll) in self.ticks :
                self.mtext ( ii, ll )

            self.mkHline ( self.height-1 )
            for ix in self.xticks :
                self.xtick ( ix )

        # Called by timer (or button)
        def update ( self ) :
            #print ( "left update" )

            # This must be a method in the superclass (Panel)
            w, h = self.GetSize()
            #print ( "w,h = ", w, h )

            if w < 100 or h < 100 :
                return

            self.xy = self.data.getxy ()
            self.ticks = self.data.getticks ()
            self.xticks = self.data.get_xticks ()

            # trigger a repaint
            self.Refresh ()

# ----------------------------------------------------------------------------------------------

bogus = "          ---"

# The right panel has text information and two useless buttons
class Right_Panel ( wx.Panel ) :
        def __init__ ( self, parent, data, left ) :
            wx.Panel.__init__ ( self, parent )

            self.data = data
            self.left = left

            #self.SetBackgroundColour ( wx.GREEN )

            rsz = wx.BoxSizer ( wx.VERTICAL )
            self.SetSizer ( rsz )

            # Ought to match radio buttons below
            self.day_list = [ 1, 2, 3, 7]

            self.r = []
            rb = wx.RadioButton ( self, 1, label="24 hour", pos=(10,10), style=wx.RB_GROUP )
            rsz.Add ( rb, 1, wx.EXPAND )
            self.r.append ( rb )

            rb = wx.RadioButton ( self, 2, label="48 hour", pos=(10,30) )
            rsz.Add ( rb, 1, wx.EXPAND )
            self.r.append ( rb )

            rb = wx.RadioButton ( self, 3, label="72 hour", pos=(10,50) )
            rsz.Add ( rb, 1, wx.EXPAND )
            self.r.append ( rb )

            rb = wx.RadioButton ( self, 7, label="week", pos=(10,70) )
            rsz.Add ( rb, 1, wx.EXPAND )
            self.r.append ( rb )

            # This can (and should) throw an exception if
            #  the search fails
            x = self.day_list.index ( default_days )
            self.r[x].SetValue ( True )

            self.Bind ( wx.EVT_RADIOBUTTON, self.onRadio )

            bup = wx.Panel ( self, -1 )
            self.b_up = wx.Button ( bup, wx.ID_ANY, "Update")
            self.b_up.Bind ( wx.EVT_BUTTON, self.onUpdate )
            self.b_ex = wx.Button ( bup, wx.ID_ANY, "Exit")
            self.b_ex.Bind ( wx.EVT_BUTTON, self.onExit )
            bus = wx.BoxSizer ( wx.HORIZONTAL )
            bus.Add ( self.b_up, 1, wx.EXPAND )
            bus.Add ( self.b_ex, 1, wx.EXPAND )
            bup.SetSizer ( bus )

            rsz.Add ( bup, 1, wx.EXPAND )

            #self.t_cur_date = MyStaticText ( self, wx.ID_ANY, bogus )
            #rsz.Add ( self.t_cur_date, 1, wx.EXPAND )

            EZtext ( self, rsz, " " )
            self.t_cur_date = EZtext ( self, rsz, bogus )
            self.t_prior_temp = EZtext ( self, rsz, bogus )
            self.t_cur_temp = EZtext ( self, rsz, bogus )
            self.t_cur_hum = EZtext ( self, rsz, bogus )
            self.t_sunrise = EZtext ( self, rsz, bogus )
            self.t_sunset = EZtext ( self, rsz, bogus )
            self.t_high = EZtext ( self, rsz, bogus )
            self.t_low = EZtext ( self, rsz, bogus )
            self.t_battery = EZtext ( self, rsz, bogus )

            self.data.gather_data ( default_days )
            self.update ( True )
            self.left.update ();

        def onRadio ( self, event ) :
            rb = event.GetEventObject ()
            lab = rb.GetLabel ()
            #print ( "Radio EVENT ****************** ", lab )
            #val = rb.GetValue ()    # is always true
            if lab.startswith ( '24' ) :
                x = 1;
            if lab.startswith ( '48' ) :
                x = 2;
            if lab.startswith ( '72' ) :
                x = 3;
            if lab.startswith ( 'we' ) :
                x = 7;

            self.data.gather_data ( x )
            self.update ( True )
            self.left.update ()

        # Called by timer (or button)
        def update ( self, new_data ) :

            dt_now = datetime.datetime.now()
            # dt_now = datetime.datetime.today()
            #print ( dt_now )

            # day_and_time = dt_now.strftime( "%B %d %Y")
            day_and_time = dt_now.strftime( "%B %d %Y") + "  " + dt_now.strftime("%H:%M:%S")

            self.t_cur_date.SetLabel ( day_and_time )
            #self.t_cur_time.SetLabel ( dt_now.strftime("%H:%M:%S") )

            if ( not new_data ) :
                return

            info = self.data.get_info()

            if self.data.is_dead () :
                self.t_prior_temp.SetLabel ( " --- --- " )
                self.t_cur_temp.SetLabel ( " -  DEAD battery - " )
                #self.t_cur_temp.SetForegroundColour ( wx.RED )
                self.t_cur_temp.SetFGColour ( wx.RED )
                self.t_cur_hum.SetLabel ( " --- --- " )
            else :
                self.t_cur_temp.SetLabel ( " Today: " + info['today'] + "F" )
                self.t_prior_temp.SetLabel ( "Yesterday: " + info['prior'] + "F" )
                self.t_cur_hum.SetLabel ( info['hum'] + " %" )

            self.t_sunrise.SetLabel ( "Sunrise: " + info['sunrise'] )
            self.t_sunset.SetLabel ( "Sunset: " + info['sunset'] )

            self.t_high.SetLabel ( info['high'] + " high" )
            self.t_low.SetLabel ( info['low'] + " low" )

            self.t_battery.SetLabel ( "Battery: " + info['battery'] )

        # Tkinter was always a pain in the ass wanting you
        # to call a destroy method and spewing out weird messages
        # whatever you did. wxPython just works nicely if you do this.
        def onExit ( self, event ) :
            sys.exit ()

        def onUpdate ( self, event ) :
            print ( "Useless update button was pushed" )
            self.update ( True )
            self.left.update ()

# Check every second for new data
timer_delay = 1000    # milliseconds

class Temp_Frame (wx.Frame):
 
        def __init__ ( self, parent, title, data ):
            wx.Frame.__init__(self, None, wx.ID_ANY, title, size=wsize )
            #top = wx.Frame.__init__(self, None, wx.ID_ANY, title, pos=(a,b), size=wsize )

            self.data = data

            #splitter = wx.SplitterWindow ( self, -1 )
            splitter = wx.SplitterWindow(self, style = wx.SP_LIVE_UPDATE)

            self.lpanel = Left_Panel ( splitter, data )
            self.rpanel = Right_Panel ( splitter, data, self.lpanel )

            # only left side grows
            splitter.SetSashGravity ( 1.0 )

            splitter.SetMinimumPaneSize ( right_size )
            splitter.SplitVertically ( self.lpanel, self.rpanel )
            splitter.SetSashPosition ( split_pos, True )

            self.timer = wx.Timer ( self )
            self.Bind ( wx.EVT_TIMER, self.timer_update, self.timer )
            self.timer.Start ( timer_delay )

        # Called at 1 Hz
        def timer_update ( self, event ) :
            #print ( "Tick" )
            if self.data.new_data () :
                #print ( "Data arrived" )
                self.data.check_battery ()
                self.data.gather_data ( None )
                self.lpanel.update ()
                self.rpanel.update ( True )
            else:
                self.rpanel.update ( False )

class Temp_GUI ( wx.App ):
        def __init__ ( self ) :
            wx.App.__init__(self)
            data = Temp_Data ()
            frame = Temp_Frame ( None, "Temp Plotter", data )
            self.SetTopWindow ( frame )
            frame.Show ( True )

app = Temp_GUI ()
app.MainLoop()

# THE END
